# install.packages("R6")
library(R6)14 R6
Introduction
本章介绍R6 OOP系统,它有两大特点:
R6使用了封装的OOP范式,意味着方法(method)属于对象(object)而不是泛型函数(generic),调用方法的范式为
object$method()。R6对象是可改变的,意味着它们可以原地修改并具有引用语义。当你将一个R6对象赋值给另一个变量时,实际上是将指向该R6对象的引用来赋值给新变量。这样,任何对该对象所做的更改都会反映在所有引用它的变量中。
虽然R6 OOP系统与其他语言中的OOP范式相同,使用起来更容易上手,但它缺点就是不符合R的使用习惯,我们将在第16章中讨论它们。
Outline
- 14.2节:介绍使用
R6::R6Class()创建R6类,使用构造器$new()创建新的R6对象。 - 14.3节:讨论R6的访问机制:私有域和主动域。
- 14.4节:探讨R6的引用语义的影响。学习如何使用终结器自动清理初始化器中执行的任何操作,以及如何在另一个R6对象中将一个R6对象作为字段使用。
- 14.5节:对比R6系统和RC系统。
Prerequisites
Classes and methods
R6::R6Class()函数可以同时构建类(class)和方法(method),同时也是R6包中唯一需要使用的函数。
R6Class()函数有两个极其重要的参数:
classname:类名,它不是必须的,但它改进了错误消息,并使得R6对象可以与S3类的泛型函数结合使用。R6 class名称通常使用UpperCamelCase命名法。public:一个列表,包含类的属性(field)和方法,可以通过self$的方法获取。属性和方法通常使用snake_case命名法。
Accumulator <- R6Class(
classname = "Accumulator",
public = list(
sum = 0,
add = function(x = 1) {
self$sum <- self$sum + x
invisible(self)
}
)
)在使用R6Class()创建对象时,需要始终将创建的结果赋值给与类名相同的变量。
Accumulator
#> <Accumulator> object generator
#> Public:
#> sum: 0
#> add: function (x = 1)
#> clone: function (deep = FALSE)
#> Parent env: <environment: R_GlobalEnv>
#> Locked objects: TRUE
#> Locked class: FALSE
#> Portable: TRUE可以使用object$new()的方法创建新对象。
x <- Accumulator$new()同样地,使用$获取对的属性和方法。
x$add(4)
x$sum
#> [1] 4后续我们以()区分$获取的时属性还是方法——$add()表示方法,$sum表示属性。
Method chaining
当$add()方法返回的是self而不是$sum时,我们就可以使用方法链(method chaining),类似管道符。通常我们使用return()来返回,但鉴于self的隐私性,这里使用invisible()。
x$add(10)$add(10)$sum
#> [1] 24
x$
add(10)$
add(10)$
sum
#> [1] 44Important methods
对大多数R6对象,有两个重要的方法需要定义——$initialize()和$print()。它们非必须,但会提升对象的使用性。
$initialize()方法会覆盖默认的$new()方法。例如下面的“Person”类,我在$initialize()方法中判断了$name属性只能是单一的字符串,$age属性只能是单一的数字。如果你有更多对输入的检查,将它们放在$validate()方法中更合适。
Person <- R6Class("Person", list(
name = NULL,
age = NA,
initialize = function(name, age = NA) {
stopifnot(is.character(name), length(name) == 1)
stopifnot(is.numeric(age), length(age) == 1)
self$name <- name
self$age <- age
}
))
hadley <- Person$new("Hadley", age = "thirty-eight")
#> Error in initialize(...): is.numeric(age) is not TRUE
hadley <- Person$new("Hadley", age = 38)$print()方法会覆盖默认的print()方法,允许你自定义对象的打印输出。和其他R6对象的方法一样,最终使用invisible()来返回。
Person <- R6Class("Person", list(
name = NULL,
age = NA,
initialize = function(name, age = NA) {
self$name <- name
self$age <- age
},
print = function(...) {
cat("Person: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Age: ", self$age, "\n", sep = "")
invisible(self)
}
))
hadley2 <- Person$new("Hadley")
hadley2
#> Person:
#> Name: Hadley
#> Age: NAAdding methods after creation
可以使用$set()修改R6对象的属性和方法。
Accumulator <- R6Class("Accumulator")
Accumulator$set("public", "sum", 0)
Accumulator$set("public", "add", function(x = 1) {
self$sum <- self$sum + x
invisible(self)
})需要注意:对象添加新的属性和方法后,只有用它创建新的对象时才会添加,已经创建好的对象不会添加新的属性和方法。
Inheritance
参数inherit允许创建继承关系。
AccumulatorChatty <- R6Class(
"AccumulatorChatty",
inherit = Accumulator,
public = list(
add = function(x = 1) {
cat("Adding ", x, "\n", sep = "")
super$add(x = x)
}
)
)
x2 <- AccumulatorChatty$new()
x2$add(10)$add(1)$sum
#> Adding 10
#> Adding 1
#> [1] 11拥有继承关系的子类可以使用父类的方法,但时如何名称相同发生覆盖,则需要使用suppe$来方法父类方法,这与上一章中的NextMethod()函数类似。
Introspection
每一个R6对象中都含有一个S3类。这意味着我们可以对R6对象使用一些S3类常用的函数,上述提到的$print()方法,本质上是print.R6()函数。
class()可以确定是否属于R6类。
class(hadley2)
#> [1] "Person" "R6"names()可以查看R6类的所有属性和方法名。下面的.__enclos_env__是R6内部的实现细节(R6 = S3 + env)。
names(hadley2)
#> [1] ".__enclos_env__" "age" "name" "clone"
#> [5] "print" "initialize"Exercises
- Create a bank account R6 class that stores a balance and allows you to deposit and withdraw money. Create a subclass that throws an error if you attempt to go into overdraft. Create another subclass that allows you to go into overdraft, but charges you a fee.
solution
Bank <- R6Class("Bank", list(
name = "",
balance = 0,
initialize = function(name, balance = 0) {
stopifnot(is.character(name), length(name) == 1)
stopifnot(is.numeric(balance), length(balance) == 1)
self$name <- name
self$balance <- balance
},
print = function(...) {
cat("Bank: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Balance: ", self$balance, "\n", sep = "")
invisible(self)
},
deposit = function(x) {
self$balance <- self$balance + x
invisible(self)
},
withdraw = function(x) {
self$balance <- self$balance - x
invisible(self)
}
))
a <- Bank$new(name = "a", balance = 1000)
a$deposit(500)$withdraw(2000)
a
#> Bank:
#> Name: a
#> Balance: -500
Bank2 <- R6Class("Bank2", inherit = Bank, public = list(
withdraw = function(x) {
if (self$balance - x < 0) {
stop("Insufficient funds", call. = FALSE)
}
}
))
b <- Bank2$new(name = "b", balance = 1000)
b$deposit(500)$withdraw(2000)
#> Error: Insufficient funds
b
#> Bank:
#> Name: b
#> Balance: 1500
Bank3 <- R6Class("Bank3", inherit = Bank, public = list(
withdraw = function(x) {
if (self$balance - x < 0) {
message("charge of $5 applied")
self$balance <- self$balance - x - 5
}
}
))
c <- Bank3$new(name = "c", balance = 1000)
c$deposit(500)$withdraw(2000)
#> charge of $5 applied
c
#> Bank:
#> Name: c
#> Balance: -505- Create an R6 class that represents a shuffled deck of cards. You should be able to draw cards from the deck with $draw(n), and return all cards to the deck and reshuffle with $reshuffle(). Use the following code to make a vector of cards.
suit <- c("♠", "♥", "♦", "♣")
value <- c("A", 2:10, "J", "Q", "K")
cards <- paste0(rep(value, 4), suit)solution
ShuffledDeck <- R6Class(
classname = "ShuffledDeck",
public = list(
deck = NULL,
initialize = function(deck = cards) {
self$deck <- sample(deck)
},
reshuffle = function() {
self$deck <- sample(cards)
invisible(self)
},
n = function() {
length(self$deck)
},
draw = function(n = 1) {
if (n > self$n()) {
stop("Only ", self$n(), " cards remaining.", call. = FALSE)
}
output <- self$deck[seq_len(n)]
self$deck <- self$deck[-seq_len(n)]
output
}
)
)
my_deck <- ShuffledDeck$new()
my_deck$draw(52)
#> [1] "Q♦" "5♥" "K♦" "6♣" "K♠" "2♠" "Q♥" "J♣" "9♣" "J♦" "6♦" "5♠"
#> [13] "K♣" "2♦" "8♥" "A♥" "10♠" "4♦" "10♥" "7♦" "9♦" "3♠" "3♥" "7♥"
#> [25] "K♥" "5♦" "7♣" "8♠" "A♣" "10♣" "9♠" "6♥" "J♠" "2♥" "9♥" "A♦"
#> [37] "8♣" "A♠" "3♦" "8♦" "Q♠" "4♠" "4♣" "3♣" "4♥" "6♠" "J♥" "Q♣"
#> [49] "5♣" "7♠" "10♦" "2♣"
my_deck$draw(10)
#> Error: Only 0 cards remaining.
my_deck$reshuffle()$draw(5)
#> [1] "10♣" "5♦" "8♥" "K♦" "2♠"
my_deck$reshuffle()$draw(5)
#> [1] "K♦" "4♥" "9♣" "J♠" "K♥"Controlling access
R6Class()函数有两个与public参数类似的参数:
private:创建的R6对象的私有属性和方法,只允许对象内部访问。active:创建的R6对象的动态属性,通过 accessor 函数访问。
Privacy
private参数创建的私有属性和方法有两个特点:
- 创建方式与
public参数一样,都是一个带有name的list。 - 在对象内部调用时,需要使用
private$前缀。
下面是一个私有属性示例:
Person <- R6Class("Person",
public = list(
initialize = function(name, age = NA) {
private$name <- name
private$age <- age
},
print = function(...) {
cat("Person: \n")
cat(" Name: ", private$name, "\n", sep = "")
cat(" Age: ", private$age, "\n", sep = "")
}
),
private = list(
age = NA,
name = NULL
)
)
hadley3 <- Person$new("Hadley")
hadley3
#> Person:
#> Name: Hadley
#> Age: NA
hadley3$name
#> NULL相交于其他语言,私有方法在R语言中通常不是很重要。
Active fields
动态属性看起来像是公共属性,但实际是由一个active binding函数定义。active binding函数只有一个参数value,如果参数missing(), 则检索该值;否则,将对其进行修改。
下例定义了动态属性random,每次访问时,会返回一个随机数。
Rando <- R6::R6Class("Rando", active = list(
random = function(value) {
if (missing(value)) {
runif(1)
} else {
stop("Can't set `$random`", call. = FALSE)
}
}
))
x <- Rando$new()
x$random(3)
#> Error: attempt to apply non-function
x$random
#> [1] 0.197574
x$random <- 31
#> Error: Can't set `$random`动态属性可以使静态属性看起来像公共属性。例如下例中,我们创建了只读的属性age和能确保字符串长度为1的属性name。
Person <- R6Class("Person",
private = list(
.age = NA,
.name = NULL
),
active = list(
age = function(value) {
if (missing(value)) {
private$.age
} else {
stop("`$age` is read only", call. = FALSE)
}
},
name = function(value) {
if (missing(value)) {
private$.name
} else {
stopifnot(is.character(value), length(value) == 1)
private$.name <- value
self
}
}
),
public = list(
initialize = function(name, age = NA) {
private$.name <- name
private$.age <- age
}
)
)
hadley4 <- Person$new("Hadley", age = 38)
hadley4$name
#> [1] "Hadley"
hadley4$name <- "Hadley2"
hadley4$name <- 10
#> Error in (function (value) : is.character(value) is not TRUE
hadley4$age <- 20
#> Error: `$age` is read only子类无法访问到父类的私有属性,但是可以访问到父类的私有方法:
A <- R6Class(
classname = "A",
private = list(
field = "foo",
method = function() {
"bar"
}
)
)
B <- R6Class(
classname = "B",
inherit = A,
public = list(
test = function() {
cat("Field: ", super$field, "\n", sep = "")
cat("Method: ", super$method(), "\n", sep = "")
}
)
)
B$new()$test()
#> Field:
#> Method: barReference semantics
R6 OOP系统与其他系统的最大不同就是它的引用语义。引用语义意味着对象被修改时不会被复制。
y1 <- Accumulator$new()
y2 <- y1
y1$add(10)
c(y1 = y1$sum, y2 = y2$sum)
#> y1 y2
#> 10 10如果你想要复制对象,你需要使用$clone()方法,添加参数deep = TRUE可以克隆嵌套的对象。
y1 <- Accumulator$new()
y2 <- y1$clone()
y1$add(10)
c(y1 = y1$sum, y2 = y2$sum)
#> y1 y2
#> 10 0引用语义的使用同样会带来其他结果:
- 需要更多的上下文才能理解R6对象。
- 考虑何时删除 R6对象是有意义的,你可以编写
$finalize()来补充$initialize(),。 - 如果某个属性是R6对象,则必须在
$initialize()中创建它,而不是在R6Class()中。
Reasoning
通常,参考语义会导致代码更难推理。考虑下面的例子:
x <- list(a = 1)
y <- list(b = 2)
z <- f(x, y)因为函数f内部无法修改外部的x,y,所以我们知道函数f只修改了z。
但是想象x,y是一个R6对象:
x <- List$new(a = 1)
y <- List$new(b = 2)
z <- f(x, y)函数f内部可以调用x和y内部的属性或方法,并对它们进行修改。我们无法仅从z <- f(x, y)判断函数f是否修改了x,y,我们需要查看函数f内部的代码。
Finalizer
因为R6对象具有引用语义,所以删除一次就会完全删除对象(不发生修改即拷贝)。这意味着我们可以在R6对象被删除时,使用$finalize()执行某些清理工作(类似on.exit()),来补充$initialize()。如下例中,我们实例化一个创建临时文件对象,然后删除该实例,就会删除临时文件。
TemporaryFile <- R6Class(
"TemporaryFile",
public = list(
path = NULL,
initialize = function() {
self$path <- tempfile()
}
),
private = list(
finalize = function() {
message("Cleaning up ", self$path)
unlink(self$path)
}
)
)
tf <- TemporaryFile$new()
rm(tf)
gc() # 使用gc()才会触发,书中好像是rm(tf)就会触发。
#> used (Mb) gc trigger (Mb) max used (Mb)
#> Ncells 949190 50.7 1892770 101.1 1892770 101.1
#> Vcells 2058420 15.8 8388608 64.0 3451412 26.4R6 fields
当使用R6类作为另外一个R6类的属性时,必须在$initialize方法中初始化属性。因为在外部定义的属性,表示该属性在定义R6类时已经创建,后续的所有实例都会继承这个属性。例如下面案例:我们想每次创建临时数据库时都创建一个临时文件,如果在外部定义属性file,实例db_a和db_b都会继承这个属性,这样db_a和db_b的属性file都指向同一个文件。
TemporaryDatabase <- R6Class(
"TemporaryDatabase",
public = list(
con = NULL,
file = TemporaryFile$new(),
initialize = function() {
self$con <- DBI::dbConnect(RSQLite::SQLite(), path = file$path)
}
),
private = list(
finalize = function() {
DBI::dbDisconnect(self$con)
}
)
)
db_a <- TemporaryDatabase$new()
db_b <- TemporaryDatabase$new()
db_a$file$path == db_b$file$path
#> [1] TRUE相反,使用$initialize()方法,在创建实例时,始终会重新创建属性file。
TemporaryDatabase <- R6Class(
"TemporaryDatabase",
public = list(
con = NULL,
file = NULL,
initialize = function() {
self$file <- TemporaryFile$new()
self$con <- DBI::dbConnect(RSQLite::SQLite(), path = file$path)
}
),
private = list(
finalize = function() {
DBI::dbDisconnect(self$con)
}
)
)
db_a <- TemporaryDatabase$new()
db_b <- TemporaryDatabase$new()
db_a$file$path == db_b$file$path
#> [1] FALSEWhy R6?
R6 OOP系统相较于 RC OOP系统的一些优势:
- R6 更简单。R6 基于S3,RC 基于S4。
- R6 有全面的文档。https://r6.r-lib.org/
- R6 提供了一种更简单的跨包子类化机制,这种机制无需思考就能正常工作。
- R6 对属性方法的管理更加明确。
- R6 更快。
- RC 与 base R 绑定,意味着你需要修改不同R版本的bug。
